package de.breitbeil.samsyncro.lib.connection;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;

import javax.microedition.io.Connector;
import javax.obex.ClientSession;
import javax.obex.HeaderSet;
import javax.obex.Operation;
import javax.obex.ResponseCodes;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.intel.bluetooth.BlueCoveConfigProperties;
import com.intel.bluetooth.BlueCoveImpl;
import com.intel.bluetooth.obex.OBEXClientSessionImpl;

import de.breitbeil.samsyncro.lib.domain.fs.SFile;
import de.breitbeil.samsyncro.lib.domain.fs.SFolder;
import de.breitbeil.samsyncro.lib.domain.parser.SFileParser;
import de.breitbeil.samsyncro.lib.domain.parser.SFolderParser;
import de.breitbeil.samsyncro.lib.domain.parser.SVCardParser;
import de.breitbeil.samsyncro.lib.domain.pim.SPimStorage;
import de.breitbeil.samsyncro.lib.domain.pim.SVCard;
import de.breitbeil.samsyncro.lib.util.Util;

/**
 * Bluetooth connection to Samsung mobiles similar to F480i or Corby. I think
 * the os is named gumi and the version is 2.5.
 * <p>
 * This connection gives you access to PIM data (contacts, calendar, tasks) and
 * the filesystem.
 * <p>
 * For every entity there are CRUD operations. See for example for contacts {@link #createContact(SVCard)},
 * {@link #getContacts()}, {@link #updateContact(SVCard)} and {@link #deleteContact(int)}. 
 * <p>
 * To establish the connection to the mobile you have to call {@link #openConnection(String, int)} with 
 * the correct bluetooth address and the channel set to the serial channel of your mobile.
 * <p>
 * 
 * @author Dominik Breitbeil
 *
 */
public class MObexConnection {
    
    private Log log = LogFactory.getLog(MObexConnection.class);
    
    private ClientSession obex = null;
    
    private ATConnection at;
    
    public MObexConnection() {
        log.debug("Start mobex connection with version "+Util.getVersion()+" ("+Util.getBuild()+")");
    }
    
    public void openConnection(String btaddress, int channel) {
        try {
            log.debug("Connecting to "+btaddress+":"+channel);
            BlueCoveImpl.setConfigProperty(BlueCoveConfigProperties.PROPERTY_OBEX_MTU, "2048");
            
            obex = (ClientSession) Connector.open("btgoep://" + btaddress + ":" + channel);
            
            initAtConnection();
            startObexServer();
            
            HeaderSet header = obex.createHeaderSet();
            header.setHeader(HeaderSet.TARGET, "MOBEX".getBytes());
            HeaderSet answer = obex.connect(header);
            
            if(ResponseCodes.OBEX_HTTP_OK != answer.getResponseCode()) {
                log.fatal("Could not connect to "+btaddress+":"+channel+". Response code was "+answer.getResponseCode());
                throw new ConnectionException("Could not connect to "+btaddress+":"+channel+". Response code was "+answer.getResponseCode());    
            }
            log.debug("connected.");
                
        } catch (IOException e) {
            log.fatal("Could not connect to "+btaddress+":"+channel, e);
            throw new ConnectionException("Could not connect to "+btaddress+":"+channel, e);
        }
    }
    
    protected void initAtConnection() {
        try {
            // To start the obex server we need to send normal AT commands.
            // The server will be shut down automatically if the connection
            // is terminated. So we need the same connection as the ObexClient.
            Class<?> superClass = OBEXClientSessionImpl.class.getSuperclass();
            Field outputStreamField = superClass.getDeclaredField("os");
            Field inputStreamField = superClass.getDeclaredField("is");
            
            outputStreamField.setAccessible(true);
            inputStreamField.setAccessible(true);
        
            OutputStream os = (OutputStream) outputStreamField.get(obex);
            InputStream is = (InputStream) inputStreamField.get(obex);
            
            at = new ATConnection(os, is);
        }
        catch(Exception e) {
            log.fatal("Could not create ATConnection.", e);
        }    
    }
    
    public void startObexServer() {
        String answer;
        
        answer = at.atz();
        if(!"OK".equalsIgnoreCase(answer)) {
            throw new MObexException("ATZ command failed: "+answer);
        }
        
        answer = at.getVendor();
        if(!"SAMSUNG".equalsIgnoreCase(answer)) {
            throw new MObexException("Only Samsung mobiles are supported. Did receive another vendor:"+answer);
        }
        
        answer = at.getModel();
        
        at.startObexServer();
    }
    
    public boolean closeConnection() throws IOException {
        obex.disconnect(null);
        obex.close();
        return true;
    }
    
    public ArrayList<SFolder> getFolderListing(String dir) throws IOException {
        HeaderSet header = obex.createHeaderSet();
        header.setHeader(HeaderSet.TYPE, "m-obex/fs/folder_listing");
        header.setHeader(HeaderSet.NAME, dir);
        
        Operation op = obex.get(header);
        validateErrorCode(op);
        String answerString = new String(getData(op));
        
        return SFolderParser.parseList(answerString);
    }
    
    public ArrayList<SFile> getFileListing(String dir) throws IOException {
        HeaderSet header = obex.createHeaderSet();
        header.setHeader(HeaderSet.TYPE, "m-obex/fs/file_listing");
        header.setHeader(HeaderSet.NAME, dir);
        
        Operation op = obex.get(header);
        validateErrorCode(op);
        String answerString = new String(getData(op));
        
        return SFileParser.parseList(answerString);
    }
    
    public SPimStorage getContactsCount() throws IOException {
        log.debug("Get contacts count.");
        HeaderSet header = obex.createHeaderSet();
        header.setHeader(HeaderSet.TYPE, "m-obex/contacts/count");
        header.setHeader(HeaderSet.APPLICATION_PARAMETER, new byte[]{0x01});
        
        Operation op = obex.get(header);
        
        validateErrorCode(op);
        
        byte[] answer = getData(op);
        // Bytes 0 and 1: maximum. Bytes 1 and 2 current count
        SPimStorage storage = new SPimStorage();
        storage.setMaxCount( Util.unsingnedWordToInt(answer, 0) );
        storage.setCurrentCount( Util.unsingnedWordToInt(answer, 2) );
        
        log.debug("Got contacts count "+storage);
        return storage;
    }
    
    public boolean updateContact(SVCard svcard) throws IOException {
        log.debug("Updating card "+svcard.getId());
        
        byte[] id = svcard.getIdAsWord();
        String vcard = svcard.getVcard();
        byte[] vcardByte = vcard.getBytes();
        
        HeaderSet header = obex.createHeaderSet();
        header.setHeader(HeaderSet.TYPE, "m-obex/contacts/write");
        header.setHeader(HeaderSet.APPLICATION_PARAMETER, new byte[]{0x01, id[0], id[1]});
        header.setHeader(HeaderSet.LENGTH, (long)vcardByte.length);
        // Pim Data must be send in one package.
        header.setHeader(0x49, vcardByte);
        
        Operation op = obex.put(header);
        
        validateErrorCode(op);
        
        op.close();
        log.debug("Update successful");
        return true;
    }

    public int createContact(SVCard svcard) throws IOException {
        log.debug("Create new contact.");
        String vcard = svcard.getVcard();
        byte[] vcardByte = vcard.getBytes();
         
        HeaderSet header = obex.createHeaderSet();
        header.setHeader(HeaderSet.TYPE, "m-obex/contacts/create");
        header.setHeader(HeaderSet.APPLICATION_PARAMETER, new byte[]{0x01});
        header.setHeader(HeaderSet.LENGTH, (long)(vcardByte.length));
        // Pim Data must be send in one package.
        header.setHeader(0x49, vcardByte);
        
        Operation op = obex.put(header);
//        DataOutputStream out = op.openDataOutputStream();
//        out.write(vcardByte);
//        out.close();
        
        // Parse answer
        validateErrorCode(op);
        
        byte[] id = getData(op);
        int idInt = Util.unsingnedWordToInt(id);
        
        log.debug("Contact created with id "+idInt);
        return idInt;
    }
    
    public boolean deleteContact(int contactId) throws IOException {
        log.debug("Delete contact with id "+contactId);
        byte[] id = new byte[]{ (byte) (contactId / 256), (byte) (contactId % 256) };
        
        HeaderSet header = obex.createHeaderSet();
        header.setHeader(HeaderSet.TYPE, "m-obex/contacts/delete");
        header.setHeader(HeaderSet.APPLICATION_PARAMETER, new byte[]{0x01, id[0], id[1]});
        
        Operation op = obex.put(header);
      
        // Parse answer
        validateErrorCode(op);
        
        op.close();
        log.debug("Deleted successful.");
        return true;
    }
    
    public List<SVCard> getContacts() throws IOException {
        StringBuilder answer = new StringBuilder();
        boolean completed = false;
        int count = 0;
        
        while(!completed) {
            log.debug("contacts load number "+count);
            byte high = (byte) (count / 256);
            byte low = (byte) (count % 256);

            // Create header
            HeaderSet header = obex.createHeaderSet();
            header.setHeader(HeaderSet.TYPE, "m-obex/contacts/load");
            header.setHeader(HeaderSet.APPLICATION_PARAMETER, new byte[]{0x01, high, low});
        
            // Get answer
            Operation op = obex.get(header);
            byte[] errorCode = getErrorCode(op);
            String answerString = new String(getData(op));
            
            // Save response and prepare next request
            answer.append(answerString);
            count++;
            completed = (errorCode[0] == 0 && errorCode[1] == 0);
        }
        
        try {
            return SVCardParser.parseList(answer.toString());
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
    
    protected byte[] getData(Operation op) throws IOException {
        // Get the object from the input stream
        InputStream in = op.openInputStream();
 
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        int data = in.read();
        while (data != -1) {
                out.write((byte) data);
                data = in.read();
        }
 
        // End the transaction
        in.close();
        op.close();
 
        byte[] obj = out.toByteArray();
        out.close();
        
        return obj;
    }
    
    protected byte[] getErrorCode(Operation op) throws IOException {
        HeaderSet headers = op.getReceivedHeaders();
        
        if(headers.getResponseCode() != ResponseCodes.OBEX_HTTP_OK) {
            throw new MObexException("Obex response code was "+headers.getResponseCode());
        }
        
        if(headers.getHeader(HeaderSet.APPLICATION_PARAMETER) == null) {
            throw new MObexException("Didn't recieve an error code");
        }
        else {
            byte[] samsungResponseCode = (byte[]) headers.getHeader(HeaderSet.APPLICATION_PARAMETER);
            return samsungResponseCode;
        }
    }
    
    protected void validateErrorCode(Operation op) throws IOException {
        byte[] errorCode = getErrorCode(op);
        
        if(Util.unsingnedWordToInt(errorCode) != 0) {
            log.debug("Error: "+Util.toHexString(errorCode));
            throw new MObexException("Error: "+Util.toHexString(errorCode));
        }
    }
}
